//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Foundation
import Testing
import SWBUtil

@Suite fileprivate struct FnmatchTests {
    @Test
    func basicMatch() throws {
        try assertFnmatch(pattern: "", input: "")
        try assertFnmatch(pattern: "", input: "abc", shouldMatch: false)
        try assertFnmatch(pattern: "xy", input: "xy")
        try assertFnmatch(pattern: "xy", input: "", shouldMatch: false)
        try assertFnmatch(pattern: "xy", input: "wxy", shouldMatch: false)
        try assertFnmatch(pattern: "xy", input: "xyz", shouldMatch: false)
        try assertFnmatch(pattern: "?", input: "", shouldMatch: false)
        try assertFnmatch(pattern: "?", input: "a")
        try assertFnmatch(pattern: "?", input: "z")
        try assertFnmatch(pattern: "?", input: ".")
        try assertFnmatch(pattern: "?", input: "/")
        try assertFnmatch(pattern: "?", input: "\\")
        try assertFnmatch(pattern: "?", input: "az", shouldMatch: false)
        try assertFnmatch(pattern: "?", input: "//", shouldMatch: false)
        try assertFnmatch(pattern: "x?y", input: "", shouldMatch: false)
        try assertFnmatch(pattern: "x?y", input: "y", shouldMatch: false)
        try assertFnmatch(pattern: "x?y", input: "xy", shouldMatch: false)
        try assertFnmatch(pattern: "x?y", input: "xay")
        try assertFnmatch(pattern: "x?y", input: "xzy")
        try assertFnmatch(pattern: "x?y", input: "x.y")
        try assertFnmatch(pattern: "x?y", input: "x/y")
        try assertFnmatch(pattern: "x?y", input: "xazy", shouldMatch: false)
        try assertFnmatch(pattern: "x?y", input: "x//y", shouldMatch: false)
        try assertFnmatch(pattern: "*", input: "")
        try assertFnmatch(pattern: "*", input: "a")
        try assertFnmatch(pattern: "*", input: "z")
        try assertFnmatch(pattern: "*", input: ".")
        try assertFnmatch(pattern: "*", input: "*")
        try assertFnmatch(pattern: "*", input: "/")
        try assertFnmatch(pattern: "*", input: "az")
        try assertFnmatch(pattern: "*", input: "//")
        try assertFnmatch(pattern: "*", input: "some long text")
        try assertFnmatch(pattern: "x*y", input: "", shouldMatch: false)
        try assertFnmatch(pattern: "x*y", input: "y", shouldMatch: false)
        try assertFnmatch(pattern: "x*y", input: "xy")
        try assertFnmatch(pattern: "x*y", input: "xay")
        try assertFnmatch(pattern: "x*y", input: "xzy")
        try assertFnmatch(pattern: "x*y", input: "x.y")
        try assertFnmatch(pattern: "x*y", input: "x*y")
        try assertFnmatch(pattern: "x*y", input: "x/y")
        try assertFnmatch(pattern: "x*y", input: "xazy")
        try assertFnmatch(pattern: "x*y", input: "x//y")
        try assertFnmatch(pattern: "**", input: "")
        try assertFnmatch(pattern: "**", input: "a")
        try assertFnmatch(pattern: "**", input: "z")
        try assertFnmatch(pattern: "**", input: ".")
        try assertFnmatch(pattern: "**", input: "*")
        try assertFnmatch(pattern: "**", input: "/")
        try assertFnmatch(pattern: "**", input: "az")
        try assertFnmatch(pattern: "**", input: "//")
        try assertFnmatch(pattern: "**", input: "some long text")
        try assertFnmatch(pattern: "x**y", input: "", shouldMatch: false)
        try assertFnmatch(pattern: "x**y", input: "y", shouldMatch: false)
        try assertFnmatch(pattern: "x**y", input: "xy")
        try assertFnmatch(pattern: "x**y", input: "xay")
        try assertFnmatch(pattern: "x**y", input: "xzy")
        try assertFnmatch(pattern: "x**y", input: "x.y")
        try assertFnmatch(pattern: "x**y", input: "x*y")
        try assertFnmatch(pattern: "x**y", input: "x/y")
        try assertFnmatch(pattern: "x**y", input: "xazy")
        try assertFnmatch(pattern: "x**y", input: "x//y")
        try assertFnmatch(pattern: "?*?", input: "foO")
        try assertFnmatch(pattern: "???*", input: "foO")
        try assertFnmatch(pattern: "*???", input: "foO")
        try assertFnmatch(pattern: "???", input: "foO")
        try assertFnmatch(pattern: "*", input: "foo")
        try assertFnmatch(pattern: "****", input: "foo")
        try assertFnmatch(pattern: "????", input: "foO", shouldMatch: false)
        try assertFnmatch(pattern: "??", input: "f", shouldMatch: false)
        try assertFnmatch(pattern: "f", input: "o", shouldMatch: false)
        try assertFnmatch(pattern: "/foo/bar/*/", input: "/foo/bar/baz/baz/")
        try assertFnmatch(pattern: "/foo/bar/*/????", input: "/foo/bar/baz/baz/")
        try assertFnmatch(pattern: "/foo/bar/???/*/", input: "/foo/bar/baz/baz/")
        try assertFnmatch(pattern: "x**y", input: "", shouldMatch: false)
        try assertFnmatch(pattern: "x**y", input: "y", shouldMatch: false)
        try assertFnmatch(pattern: "*.*", input: "foo.txt")
        try assertFnmatch(pattern: "*.*", input: "foo.")
        try assertFnmatch(pattern: "*.*", input: "foo", shouldMatch: false)
        try assertFnmatch(pattern: "x*y*z", input: "", shouldMatch: false)
        try assertFnmatch(pattern: "x*y*z", input: "x", shouldMatch: false)
        try assertFnmatch(pattern: "x*y*z", input: "y", shouldMatch: false)
        try assertFnmatch(pattern: "x*y*z", input: "z", shouldMatch: false)
        try assertFnmatch(pattern: "x*y*z", input: "xy", shouldMatch: false)
        try assertFnmatch(pattern: "x*y*z", input: "xz", shouldMatch: false)
        try assertFnmatch(pattern: "x*y*z", input: "yz", shouldMatch: false)
        try assertFnmatch(pattern: "x*y*z", input: "xyz")
        try assertFnmatch(pattern: "x*y*z", input: "xayz")
        try assertFnmatch(pattern: "x*y*z", input: "xybz")
        try assertFnmatch(pattern: "x*y*z", input: "xaybz")
        try assertFnmatch(pattern: "x*y*z", input: "xxyz")
        try assertFnmatch(pattern: "x*y*z", input: "xyyz")
        try assertFnmatch(pattern: "x*y*z", input: "xyzz")
        try assertFnmatch(pattern: "x*y*z", input: "xyxyz")
        try assertFnmatch(pattern: "x***??y", input: "", shouldMatch: false)
        try assertFnmatch(pattern: "x***??y", input: "xy", shouldMatch: false)
        try assertFnmatch(pattern: "x***??y", input: "x1y", shouldMatch: false)
        try assertFnmatch(pattern: "x***??y", input: "x12y")
        try assertFnmatch(pattern: "x***??y", input: "x123y")
        try assertFnmatch(pattern: "x**??*y", input: "", shouldMatch: false)
        try assertFnmatch(pattern: "x**??*y", input: "xy", shouldMatch: false)
        try assertFnmatch(pattern: "x**??*y", input: "x1y", shouldMatch: false)
        try assertFnmatch(pattern: "x**??*y", input: "x12y")
        try assertFnmatch(pattern: "x**??*y", input: "x123y")
        try assertFnmatch(pattern: "x*??**y", input: "", shouldMatch: false)
        try assertFnmatch(pattern: "x*??**y", input: "xy", shouldMatch: false)
        try assertFnmatch(pattern: "x*??**y", input: "x1y", shouldMatch: false)
        try assertFnmatch(pattern: "x*??**y", input: "x12y")
        try assertFnmatch(pattern: "x*??**y", input: "x123y")
        try assertFnmatch(pattern: "x??***y", input: "", shouldMatch: false)
        try assertFnmatch(pattern: "x??***y", input: "xy", shouldMatch: false)
        try assertFnmatch(pattern: "x??***y", input: "x1y", shouldMatch: false)
        try assertFnmatch(pattern: "x??***y", input: "x12y")
        try assertFnmatch(pattern: "x??***y", input: "x123y")
        try assertFnmatch(pattern: "x**?*?y", input: "", shouldMatch: false)
        try assertFnmatch(pattern: "x**?*?y", input: "xy", shouldMatch: false)
        try assertFnmatch(pattern: "x**?*?y", input: "x1y", shouldMatch: false)
        try assertFnmatch(pattern: "x**?*?y", input: "x12y")
        try assertFnmatch(pattern: "x**?*?y", input: "x123y")
        try assertFnmatch(pattern: "x*?*?*y", input: "", shouldMatch: false)
        try assertFnmatch(pattern: "x*?*?*y", input: "xy", shouldMatch: false)
        try assertFnmatch(pattern: "x*?*?*y", input: "x1y", shouldMatch: false)
        try assertFnmatch(pattern: "x*?*?*y", input: "x12y")
        try assertFnmatch(pattern: "x*?*?*y", input: "x123y")
        try assertFnmatch(pattern: "x?*?**y", input: "", shouldMatch: false)
        try assertFnmatch(pattern: "x?*?**y", input: "xy", shouldMatch: false)
        try assertFnmatch(pattern: "x?*?**y", input: "x1y", shouldMatch: false)
        try assertFnmatch(pattern: "x?*?**y", input: "x12y")
        try assertFnmatch(pattern: "x?*?**y", input: "x123y")
        try assertFnmatch(pattern: "x*?**?y", input: "", shouldMatch: false)
        try assertFnmatch(pattern: "x*?**?y", input: "xy", shouldMatch: false)
        try assertFnmatch(pattern: "x*?**?y", input: "x1y", shouldMatch: false)
        try assertFnmatch(pattern: "x*?**?y", input: "x12y")
        try assertFnmatch(pattern: "x*?**?y", input: "x123y")
        try assertFnmatch(pattern: "x?**?*y", input: "", shouldMatch: false)
        try assertFnmatch(pattern: "x?**?*y", input: "xy", shouldMatch: false)
        try assertFnmatch(pattern: "x?**?*y", input: "x1y", shouldMatch: false)
        try assertFnmatch(pattern: "x?**?*y", input: "x12y")
        try assertFnmatch(pattern: "x?**?*y", input: "x123y")
        try assertFnmatch(pattern: "x?***?y", input: "", shouldMatch: false)
        try assertFnmatch(pattern: "x?***?y", input: "xy", shouldMatch: false)
        try assertFnmatch(pattern: "x?***?y", input: "x1y", shouldMatch: false)
        try assertFnmatch(pattern: "x?***?y", input: "x12y")
        try assertFnmatch(pattern: "x?***?y", input: "x123y")
    }

    @Test
    func rangeMatch() throws {
        try assertFnmatch(pattern: "[a-z]", input: "", shouldMatch: false)
        try assertFnmatch(pattern: "[a-z]", input: "a")
        try assertFnmatch(pattern: "[a-z]", input: "z")
        try assertFnmatch(pattern: "[a-z][!0-9]", input: "az")
        try assertFnmatch(pattern: "[a-z][0-9]", input: "z9")
        try assertFnmatch(pattern: "[a-z]", input: ".", shouldMatch: false)
        try assertFnmatch(pattern: "[a-z]", input: "/", shouldMatch: false)
        try assertFnmatch(pattern: "[a-z]", input: "az", shouldMatch: false)
        try assertFnmatch(pattern: "[a-z]", input: "//", shouldMatch: false)
        try assertFnmatch(pattern: "[!a-z]", input: "", shouldMatch: false)
        try assertFnmatch(pattern: "[!a-z]", input: "a", shouldMatch: false)
        try assertFnmatch(pattern: "[!a-z]", input: "z", shouldMatch: false)
        try assertFnmatch(pattern: "[!a-z]", input: ".")
        try assertFnmatch(pattern: "[!a-z]", input: "/")
        try assertFnmatch(pattern: "[!a-z]", input: "az", shouldMatch: false)
        try assertFnmatch(pattern: "[!a-z]", input: "//", shouldMatch: false)
        try assertFnmatch(pattern: "[a-z]", input: "M", shouldMatch: false)
        try assertFnmatch(pattern: "[A-Z]", input: "m", shouldMatch: false)
        try assertFnmatch(pattern: "fo[Ob]", input: "foO")
        try assertFnmatch(pattern: "foO[ba]", input: "foO", shouldMatch: false)
        try assertFnmatch(pattern: "[a-z]oo", input: "foo")
        try assertFnmatch(pattern: "[a-z]oo/bar/*/baz", input: "foo/bar/baz/baz")
        try assertFnmatch(pattern: "[a-z]oo/bar/????/baz", input: "foo/bar/bazo/baz")

        #expect(throws: FnmatchError.rangeError(pattern: "[a-]")) {
            try assertFnmatch(pattern: "[a-]", input: "a")
        }

        /* Verify that an unmatched [ is treated as a literal, as POSIX requires.  */
        try assertFnmatch(pattern: "[", input: "[")
        try assertFnmatch(pattern: "[", input: "A", shouldMatch: false)
    }

    @Test
    func forwardAndBackSlashMatch() throws {
        // these test that '\' is handled correctly in character sets.
        try assertFnmatch(pattern: "\\foo\\bar\\*\\", input: "\\foo\\bar\\baz\\baz\\")
        try assertFnmatch(pattern: #"[\]"#, input: #"\"#)
        try assertFnmatch(pattern: #"[!\]"#, input: "a")
        try assertFnmatch(pattern: #"[!\]"#, input: #"\"#, shouldMatch: false)
        try assertFnmatch(pattern: "foo\\.txt", input: "foo.txt", shouldMatch: false)
        try assertFnmatch(pattern: "x?y", input: "x/y")
        try assertFnmatch(pattern: "x\\?y", input: "x/y", shouldMatch: false)
        try assertFnmatch(pattern: "x\\?y", input: "x?y", shouldMatch: false)
        try assertFnmatch(pattern: "x\\?y", input: "x\\1y")
        try assertFnmatch(pattern: "x*y", input: "x/y")
        try assertFnmatch(pattern: "x\\*y", input: "x/y", shouldMatch: false)
        try assertFnmatch(pattern: "x\\*y", input: "x/y", shouldMatch: false)
        try assertFnmatch(pattern: "x*y", input: "x\\*y")
        try assertFnmatch(pattern: "x\\*y", input: "x\\*y")

        try assertFnmatch(pattern: "/foo/bar/*/", input: "/foo/bar/baz/baz/")
        try assertFnmatch(pattern: "[/]", input: "/")
        try assertFnmatch(pattern: "[!/]", input: "a")
        try assertFnmatch(pattern: "[!/]", input: "/", shouldMatch: false)
        try assertFnmatch(pattern: "foo/.txt", input: "foo.txt", shouldMatch: false)
        try assertFnmatch(pattern: "x?y", input: "x/y")
        try assertFnmatch(pattern: "x/?y", input: "x\\y", shouldMatch: false)
        try assertFnmatch(pattern: "x/?y", input: "x?y", shouldMatch: false)
        try assertFnmatch(pattern: "x/?y", input: "x/1y")
        try assertFnmatch(pattern: "x*y", input: "x\\y")
        try assertFnmatch(pattern: "x/*y", input: "x/", shouldMatch: false)
        try assertFnmatch(pattern: "x/*y", input: "x\\/y", shouldMatch: false)
        try assertFnmatch(pattern: "x*y", input: "x/*y")
        try assertFnmatch(pattern: "x/*y", input: "x/*y")

    }

    @Test
    func caseInsensitivityMatch() throws {
        try assertFnmatch(pattern: "foo", input: "foO", options: [.caseInsensitive])
        try assertFnmatch(pattern: "fo[ob]", input: "foO", options: [.caseInsensitive])
        try assertFnmatch(pattern: "fo[!BA]", input: "foO", options: [.caseInsensitive])
        try assertFnmatch(
            pattern: "foO[ba]", input: "foo", shouldMatch: false, options: [.caseInsensitive])
        try assertFnmatch(
            pattern: "fo[oO]/???/*/baz", input: "foO/bar/baz/baz", options: [.caseInsensitive])
        try assertFnmatch(pattern: "[A-Z]", input: "m", options: [.caseInsensitive])
        try assertFnmatch(pattern: "[a-z]", input: "M", options: [.caseInsensitive])
    }

    @Test(arguments: [true, false])
    func pathnameMatch(isWindows: Bool) throws {
        let separators = isWindows ? "\\/" : "/"

        try assertFnmatch(pattern: "x?y", input: "x/y", separators: separators)
        try assertFnmatch(pattern: "x?y", input: "x/y", shouldMatch: false, options: [.pathname], separators: separators)
        try assertFnmatch(pattern: "x*y", input: "x/y", separators: separators)
        try assertFnmatch(pattern: "x*y", input: "x/y", shouldMatch: false, options: [.pathname], separators: separators)
        try assertFnmatch(pattern: "[/-/]", input: "/", separators: separators)
        try assertFnmatch(pattern: "[/=/]", input: "a/", shouldMatch: false, options: [.pathname], separators: separators)
        try assertFnmatch(pattern: "[/=/]", input: "/", shouldMatch: false, options: [.pathname], separators: separators)
        try assertFnmatch(pattern: "[!-~]", input: "/", shouldMatch: false, options: [.pathname], separators: separators)

        try assertFnmatch(pattern: "foo/bar/*/boo", input: "foo/bar/baz/boo", options: [.pathname], separators: separators)
        try assertFnmatch(pattern: "foo/bar/*/???", input: "foo/bar/baz/boo", options: [.pathname], separators: separators)

        try assertFnmatch(pattern: "foo/bar/*/???", input: "foo/bar/baz/", shouldMatch: false, options: [.pathname], separators: separators)
        try assertFnmatch(pattern: "foo/bar/*", input: "foo/bar/baz/", shouldMatch: false, options: [.pathname], separators: separators)
        try assertFnmatch(pattern: "foo/bar/*", input: "foo/bar/baz", shouldMatch: true, options: [.pathname], separators: separators)
        try assertFnmatch(pattern: "foo/bar/*/", input: "foo/bar/baz", shouldMatch: false, options: [.pathname], separators: separators)
        try assertFnmatch(pattern: "foo/bar/*/", input: "foo/bar/baz/", shouldMatch: true, options: [.pathname], separators: separators)

        try assertFnmatch(pattern: "foo/bar/*/boo", input: #"foo\bar\baz\boo"#, shouldMatch: isWindows, options: [.pathname], separators: separators)
        try assertFnmatch(pattern: "foo/bar/*/???", input: #"foo\bar\baz\boo"#, shouldMatch: isWindows, options: [.pathname], separators: separators)
    }

    func assertFnmatch(
        pattern: String, input: String, shouldMatch: Bool = true, options: FnmatchOptions = .default, separators: (some Collection<Character>)? = ([Character]?).none, sourceLocation: SourceLocation = #_sourceLocation) throws {
        let comment = Comment(stringLiteral: "\(pattern) \(shouldMatch ? "should" : "should not") match \(input)")
        let result = try fnmatch(pattern: pattern, input: input, options: options, pathSeparators: separators)
        shouldMatch ? #expect(result, comment, sourceLocation: sourceLocation) : #expect(!result, comment, sourceLocation: sourceLocation)
    }
}
